package codediff;

import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.diff.*;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.patch.FileHeader;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import org.eclipse.jgit.treewalk.filter.TreeFilter;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class CodeDiff {
    private static final String TAG_CANNOT_BE_NULL = "tag不能为空";
    private GitAdapter gitAdapter;

    public CodeDiff(GitAdapter gitAdapter) {
        this.gitAdapter = gitAdapter;
    }

    /**
     * (tag...HEAD]: 大于tag
     */
    public List<IncrClassInfo> gtTag(String tagOrSha) throws GitAPIException {
        Assert.isTrue(StringUtils.isNotBlank(tagOrSha), TAG_CANNOT_BE_NULL);
        System.out.println("run gt tag: " + tagOrSha);
        CanonicalTreeParser oldTreeIter = gitAdapter.getTreeIterator(tagOrSha);
        CanonicalTreeParser newTreeIter = gitAdapter.getTreeIterator(Constants.HEAD);
        List<DiffEntry> diffEntries = gitAdapter.diff().setOldTree(oldTreeIter).setNewTree(newTreeIter).setShowNameAndStatusOnly(true).call();
        return parseDiffEntries(diffEntries, tagOrSha, Constants.HEAD);
    }

    /**
     * (oldTag...newTag]: 不包含oldTag, 包含newTag. 注意：两个tag最好在同一分支
     */
    public List<IncrClassInfo> diffTags(String oldTagOrSha, String newTagOrSha) throws GitAPIException {
        Assert.isTrue(StringUtils.isNotBlank(oldTagOrSha) && StringUtils.isNotBlank(newTagOrSha), TAG_CANNOT_BE_NULL);
        System.out.println(String.format("run diff tag [%s...%s]", oldTagOrSha, newTagOrSha));
        CanonicalTreeParser oldTreeIter = gitAdapter.getTreeIterator(oldTagOrSha);
        CanonicalTreeParser newTreeIter = gitAdapter.getTreeIterator(newTagOrSha);
        List<DiffEntry> diffEntries = gitAdapter.diff()
                .setOldTree(oldTreeIter)
                .setNewTree(newTreeIter)
                .setShowNameAndStatusOnly(true)
                .setPathFilter(TreeFilter.ANY_DIFF)
                .call();
        return parseDiffEntries(diffEntries, oldTagOrSha, newTagOrSha);
    }

    /**
     * ...tag]: 小于等于tag
     */
    public List<IncrClassInfo> leTag(String tagOrSha) throws GitAPIException {
        Assert.isTrue(StringUtils.isNotBlank(tagOrSha), TAG_CANNOT_BE_NULL);
        System.out.println("run let tag: " + tagOrSha);
        EmptyTreeIterator emptyTreeIterator = new EmptyTreeIterator();
        CanonicalTreeParser newTreeIter = gitAdapter.getTreeIterator(tagOrSha);
        List<DiffEntry> diffEntries = gitAdapter.diff().setOldTree(emptyTreeIterator).setNewTree(newTreeIter).setShowNameAndStatusOnly(true).call();
        return parseDiffEntries(diffEntries, null, tagOrSha);
    }

    /**
     * 没啥用
     */
    private List<DiffEntry> filterMovePackages(List<DiffEntry> diffEntries) {
        Map<String, String> newAdds = diffEntries.stream()
                .filter(it -> it.getChangeType() == DiffEntry.ChangeType.ADD)
                .collect(Collectors.toMap(DiffEntry::getNewPath, diffEntry -> diffEntry.getNewMode().toString()));
        List<String> moveEntries = diffEntries.stream()
                .filter(it -> it.getChangeType() == DiffEntry.ChangeType.DELETE)
                .filter(it -> newAdds.containsKey(it.getOldMode().toString()))
                .map(DiffEntry::getOldPath)
                .collect(Collectors.toList());
        return diffEntries.stream()
                .filter(it -> moveEntries.contains(it.getOldPath()) || moveEntries.contains(it.getNewPath()))
                .collect(Collectors.toList());
    }

    private List<IncrClassInfo> parseDiffEntries(List<DiffEntry> diffEntries, String oldTag, String newTag) {
        if (CollectionUtils.isEmpty(diffEntries)) {
            System.out.println("diffEntries is empty");
            return Collections.emptyList();
        }
        // TODO: 2021/11/20 check多线程是否有问题
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        DiffFormatter diffFormatter = new DiffFormatter(out);
        diffFormatter.setDiffComparator(RawTextComparator.WS_IGNORE_TRAILING);
        diffFormatter.setRepository(gitAdapter.getRepository());
        // TODO: 2021/11/20 close diffFormatter
        return prepareDiffMethodForTag(gitAdapter, oldTag, newTag, diffFormatter, diffEntries);
    }

    private List<IncrClassInfo> prepareDiffMethodForTag(GitAdapter gitAdapter, String oldTag, String newTag,
                                                        DiffFormatter diffFormatter, List<DiffEntry> diffEntries) {
        return Observable.fromIterable(diffEntries)
                .filter(this::filterSomeThing)
                .buffer(50)
                .flatMap(buffDiffEntries -> Observable.fromIterable(buffDiffEntries)
                        .observeOn(Schedulers.io())
                        .map(diffEntry -> Optional.ofNullable(prepareDiffMethodForTag(gitAdapter, oldTag, newTag, diffFormatter, diffEntry)))
                        .filter(Optional::isPresent)
                        .map(Optional::get), 8)
                .toList()
                .blockingGet();
    }

    private boolean filterSomeThing(DiffEntry diffEntry) {
        if (diffEntry.getChangeType() == DiffEntry.ChangeType.DELETE || diffEntry.getChangeType() == DiffEntry.ChangeType.RENAME) {
            return false;
        }
        String newPath = diffEntry.getNewPath();
        if (newPath.contains("src/test/java") || newPath.contains(".spec.ts")) {
            return false;
        }
        if (newPath.endsWith(".java") || newPath.endsWith(".ts")) {
            return true;
        }
        return false;
    }

    private IncrClassInfo prepareDiffMethodForTag(GitAdapter gitAdapter, String oldTag, String newTag, DiffFormatter diffFormatter, DiffEntry diffEntry) {
        String newPath = diffEntry.getNewPath();
        try {
            String newContent = new String(gitAdapter.getTagRevisionSpecificFileContent(newTag, newPath));
            ASTGenerator newAstGenerator = new ASTGenerator(newContent);
            if (diffEntry.getChangeType() == DiffEntry.ChangeType.ADD) {
                List<String> committers = gitAdapter.getCommitters(null, gitAdapter.getObjectId(newTag), newPath);
                return newAstGenerator.generateAddClassInfo(committers);
            }
            Map<String, MethodDeclaration> oldMethodsMap = generateOldMethodsMap(gitAdapter, oldTag, diffEntry);
            List<IncrMethodInfo> incrMethodInfos = Arrays.stream(newAstGenerator.getMethods())
                    .filter(newMethod -> !oldMethodsMap.containsKey(getMethodDesc(newMethod))
                            || !ASTGenerator.isMethodTheSame(newMethod, oldMethodsMap.get(getMethodDesc(newMethod))))
                    .map(newAstGenerator::mapToIncrMethodInfo)
                    .collect(Collectors.toList());
            List<String> committers = gitAdapter.getCommitters(gitAdapter.getObjectId(oldTag), gitAdapter.getObjectId(newTag), newPath);
            Pair<List<Integer>, List<Integer>> modifyLines = generateModifyLines(diffFormatter, diffEntry);
            return newAstGenerator.generateModifyClassInfo(incrMethodInfos, modifyLines.getLeft(), modifyLines.getRight(), committers);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * left：old-dels
     * right：new-adds
     */
    private Pair<List<Integer>, List<Integer>> generateModifyLines(DiffFormatter diffFormatter, DiffEntry diffEntry) throws IOException {
        FileHeader fileHeader = diffFormatter.toFileHeader(diffEntry);
        Stream<Integer> addLines = Stream.empty();
        Stream<Integer> delLines = Stream.empty();
        EditList edits = fileHeader.toEditList();
        for (Edit edit : edits) {
            if (edit.getLengthA() > 0) {
                delLines = Stream.concat(delLines, IntStream.rangeClosed(edit.getBeginA(), edit.getEndA()).boxed());
            }
            if (edit.getLengthB() > 0) {
                addLines = Stream.concat(addLines, IntStream.rangeClosed(edit.getBeginB(), edit.getEndB()).boxed());
            }
        }
        return Pair.of(delLines.collect(Collectors.toList()), addLines.collect(Collectors.toList()));
    }

    private Map<String, MethodDeclaration> generateOldMethodsMap(GitAdapter gitAdapter, String oldTag, DiffEntry diffEntry) throws IOException {
        if (StringUtils.isBlank(oldTag)) {
            return Collections.emptyMap();
        }
        String oldPath = diffEntry.getOldPath();
        String oldClassContent = new String(gitAdapter.getTagRevisionSpecificFileContent(oldTag, oldPath));
        ASTGenerator oldAstGenerator = new ASTGenerator(oldClassContent);
        // TODO: 2021/11/20 方法的变更是否需要忽略入参和注解？
        return Arrays.stream(oldAstGenerator.getMethods()).collect(Collectors.toMap(this::getMethodDesc, method -> method));
    }

    public String getMethodDesc(MethodDeclaration method) {
        return method.getName().toString() + method.parameters().toString();
    }
}